Skip to content

feat!: TCP/TLS wrap-based interception#770

Draft
kettanaito wants to merge 162 commits intomainfrom
feat/net-v2
Draft

feat!: TCP/TLS wrap-based interception#770
kettanaito wants to merge 162 commits intomainfrom
feat/net-v2

Conversation

@kettanaito
Copy link
Copy Markdown
Member

@kettanaito kettanaito commented Feb 23, 2026

Changes

  • Implements a SocketInterceptor that intercepts the outgoing socket packets on the net.connect()/tls.connect() levels. The interception mostly works via a "server-side socket" instance and patching TCPWrap/TLSWrap JavaScript bindings for C++ network code (socket._handle) to pend the connection until the interceptor decides what to do with it.
  • Implements a new HttpRequestInterceptor to replace ClientRequestInterceptor.

⚠️ Breaking changes

  • getRawRequest utility has been removed. Use the initiator reference directly.
  • /utils/node and getClientRequestBodyStream has been removed. Use the initiator reference directly.

Features

  • Supports CONNECT requests (fixes Support "CONNECT" requests (in proxies) #481, closes feat(wip): support "CONNECT" request #621).
  • Supports Undici fetch and request even when they aren't global.
  • Exposes the request initiator on the "request" event listener. The initiator property points to ClientRequest, XMLHttpRequest, or Request instance that initiated the intercepted request. If no instance can be reliably deduced, points to the raw net.Socket behind the connection.
  • TLS connections now correctly emit the keylog event on the socket.
  • XMLHttpRequest (JSDOM): Correctly performs the OPTIONS preflight request based on the JSDOM implementation (previously, preflight triggered for requests that shouldn't trigger it).
  • XMLHttpRequest: Implements mock response handling in accordance to the WHATWG XMLHttpRequest specification to improve browser compliance.

Bug fixes

  • The library now executes more of the Node.js network that ever before. In fact, it executes the biggest possible amount of Node.js code. We are literally patching before the C++ bindings.
  • ClientRequest using sockets is now properly supported.
  • Now it doesn't matter how you import http/https (e.g. import * as http broke previously since we cannot patch that).
  • You can now end requests in the write callbacks. The documented limitation of Interceptors is gone.
  • Fixes an issue where the automatic Content-Type header added by Undici was not set on mocked responses.
  • XMLHttpRequest: Fixes an issue where redirect responses weren't being followed.
  • XMLHttpRequest: Fixes an issue where the "progress" event for request.upload wasn't dispatched correctly.
  • XMLHttpRequest: total response body length is now computed only from the content-length mocked response header, just like it's done natively.
  • XMLHttpRequest: The progress event is now correctly emitted on the request for chunked responses.

Todos

  • Migrate all the existing tests to the new interceptor.
  • Add a thorough compliance test suite for SocketInterceptor. Cover behaviors like socket re-usage, event emission, properties (especially on TLS sockets), etc.
  • TLS: The session event shouldn't be emitted for reused sessions. Mocks always emit it now. options.session should be respected. Afaik, if provided, that is a pointer to a previously active session.
  • Add a test for CONNECT requests (see Support "CONNECT" requests (in proxies) #481 / feat(wip): support "CONNECT" request #621). Might they be supported now?
  • Finish logging.
  • Add tests to ensure that multiple interceptors using SocketInterceptor can run at the same time without conflicting.
  • Bug: TypeError: Second argument must be a buffer. Reproduce: pnpm run test:node test/modules/http multiple times in a row. Flaky.
  • Add http compliance test that calls req.end() in the socket's connect event.
  • Support global fetch (Undici fetch) in the HttpRequestInterceptor (currently times out).
  • Ensure passthrough HTTPs for global fetch works. Our test server has an expired certificate and fetch doesn't provide the means to set rejectUnauthorized: false so requests fail.
  • Consider if HttpRequestInterceptor needs to have the response decompression and redirect following logic. It likely doesn't. Just wonder how we can implement FetchInterceptor on top of it then.
    • Decompression—no. Undici will do that for us.
    • Redirects—don't know, needs discovery.
  • Implement the initiator property on the request event.
    • http
    • XMLHttpRequest
    • fetch
    • Remove getRawRequest
  • For the browser versions of fetch and XHR interceptors, still provide initiator. Refactor what setRawRequest did before.
  • Export node and inject versions of XHR and fetch interceptors. In Node.js, HttpRequestInterceptor now intercepts everything and more specialized interceptors only provide the initiator via async context. In the browser, however, the /inject version of the interceptors has to be used instead.
  • Remove controller.errorWith() because it's more semantically valid to use socket.destroy(). It provides no additional functionality and is a 1-to-1 replacement.
  • applyPatch: Use Symbol.for() instead of Symbol() to account for isolated environments (e.g. browser extensions (see there are problems while multiple browser extension use new XMLHttpRequestInterceptor() #771).
  • Add export conditions for fetch/XHR interceptors:
    • In the browser, resolve to the patch version.
    • In Node.js, use the /node.ts version (more powerful).
  • "finish" is not emitted when the socket writes end. A bug. Causes the related test to fail and also results in "Premature close" from some XHR scenarios since close while writable means premature close.
  • ⚠️ Migrate to use llhttp the same way Undici uses it. That's the most performant and reliable way of introducing a parser to the stack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant